#region Imported Namespaces


using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using System.Windows.Interop;

using WtfManager.WindowsNative.Shell32;
using WtfManager.WindowsNative.User32;


#endregion


namespace WtfManager.WindowsNative
{
  public class ShellContextMenu: IDisposable
  {
    #region Fields


    private const uint _firstCommand = 1;
    private const uint _lastCommand = 30000;
    private const int _maxPathLength = 260;
    private const int S_OK = 0;

    private static readonly int InvokeCommandInfoSize = Marshal.SizeOf(typeof(CMINVOKECOMMANDINFOEX));

    private static Guid IID_IContextMenu = new Guid("{000214e4-0000-0000-c000-000000000046}");
    private static Guid IID_IContextMenu2 = new Guid("{000214f4-0000-0000-c000-000000000046}");
    private static Guid IID_IContextMenu3 = new Guid("{bcfce0a0-ec17-11d0-8d10-00a0c90f2719}");
    private static Guid IID_IShellFolder = new Guid("{000214E6-0000-0000-C000-000000000046}");

    private readonly List<string> _fileSystemItemsNames;
    private readonly Window _window;
    private readonly WindowInteropHelper _windowInteropHelper;

    private IContextMenu _contextMenu;
    private IContextMenu2 _contextMenu2;
    private IContextMenu3 _contextMenu3;

    private IntPtr _contextMenuPtr;
    private IntPtr _contextMenuPtr2;
    private IntPtr _contextMenuPtr3;
    private IShellFolder _desktopFolder;
    private List<ShellContextMenuItem> _items;
    private IntPtr _menuHandle;
    private IShellFolder _parentFolder;
    private IntPtr[] _pidls;


    #endregion


    #region Constructors/Finalizers


    public ShellContextMenu(Window currentWindow, string parentFolderPath, List<string> fileSystemItemsNames)
    {
      ParentFolderPath = parentFolderPath;

      _window = currentWindow;
      _fileSystemItemsNames = fileSystemItemsNames;
      _windowInteropHelper = new WindowInteropHelper(currentWindow);

      WindowHookHelper.AddHook(_window, WndProc);
    }


    ~ShellContextMenu()
    {
      Dispose(false);
    }


    #endregion


    #region Properties/Indexers


    public List<string> FileSystemItemsNames
    {
      get
      {
        return new List<string>(_fileSystemItemsNames);
      }
    }


    public List<ShellContextMenuItem> Items
    {
      get
      {
        return new List<ShellContextMenuItem>(_items);
      }
    }

    public string ParentFolderPath
    {
      get;
      private set;
    }


    #endregion


    #region Methods


    private static IContextMenu GetContextMenuInterface(IShellFolder parentFolder, IntPtr[] pidls,
                                                        out IntPtr contextMenuPtr)
    {
      int nResult = parentFolder.GetUIObjectOf(IntPtr.Zero, (uint)pidls.Length, pidls, ref IID_IContextMenu, IntPtr.Zero,
                                               out contextMenuPtr);

      if (nResult == S_OK)
      {
        return (IContextMenu)Marshal.GetTypedObjectForIUnknown(contextMenuPtr, typeof(IContextMenu));
      }

      contextMenuPtr = IntPtr.Zero;

      Marshal.ThrowExceptionForHR(nResult);

      return null;
    }


    private static void InvokeCommand(IContextMenu contextMenu, uint commandId, string folderPath, POINT pointInvoke)
    {
      var invokeInfo = new CMINVOKECOMMANDINFOEX();
      invokeInfo.cbSize = InvokeCommandInfoSize;
      invokeInfo.lpVerb = (IntPtr)(commandId - _firstCommand);
      invokeInfo.lpDirectory = folderPath;
      invokeInfo.lpVerbW = (IntPtr)(commandId - _firstCommand);
      invokeInfo.lpDirectoryW = folderPath;
      //      invoke.fMask = CMIC.UNICODE | CMIC.PTINVOKE | ((Control.ModifierKeys & Keys.Control) != 0 ? CMIC.CONTROL_DOWN : 0)
      //                     | ((Control.ModifierKeys & Keys.Shift) != 0 ? CMIC.SHIFT_DOWN : 0);

      invokeInfo.fMask = CMIC.UNICODE | CMIC.PTINVOKE;

      invokeInfo.ptInvoke = new POINT(pointInvoke.X, pointInvoke.Y);
      invokeInfo.nShow = SW.SHOWDEFAULT;

      int result = contextMenu.InvokeCommand(ref invokeInfo);

      if (result != S_OK)
      {
        Marshal.ThrowExceptionForHR(result);
      }
    }


    /// <summary>
    /// Gets the desktop folder
    /// </summary>
    /// <returns>IShellFolder for desktop folder</returns>
    private static IShellFolder GetDesktopFolder()
    {
      IntPtr desktopFolderPtr;
      int result = SHGetDesktopFolder(out desktopFolderPtr);

      if (S_OK != result)
      {
        Marshal.ThrowExceptionForHR(result);
      }

      return (IShellFolder)Marshal.GetTypedObjectForIUnknown(desktopFolderPtr, typeof(IShellFolder));
    }


    private static IShellFolder GetParentFolder(IShellFolder desktopFolder, string folderName)
    {
      IntPtr pidl = IntPtr.Zero;
      IntPtr displayNamePtr = IntPtr.Zero;

      try
      {
        uint numCharsEaten = 0;
        SFGAO attributes = 0;

        int result = desktopFolder.ParseDisplayName(IntPtr.Zero, IntPtr.Zero, folderName, ref numCharsEaten, out pidl,
                                                    ref attributes);
        if (S_OK != result)
        {
          Marshal.ThrowExceptionForHR(result);
        }

        displayNamePtr = Marshal.AllocCoTaskMem(_maxPathLength * 2 + 4);

        Marshal.WriteInt32(displayNamePtr, 0, 0);

        result = desktopFolder.GetDisplayNameOf(pidl, SHGDN.SHGDN_FORPARSING, displayNamePtr);

        if (result != S_OK)
        {
          Marshal.ThrowExceptionForHR(result);
        }

        var displayName = new StringBuilder(_maxPathLength);
        StrRetToBuf(displayNamePtr, pidl, displayName, _maxPathLength);

        IntPtr parentFolder;
        result = desktopFolder.BindToObject(pidl, IntPtr.Zero, ref IID_IShellFolder, out parentFolder);

        if (S_OK != result)
        {
          Marshal.ThrowExceptionForHR(result);
        }

        return (IShellFolder)Marshal.GetTypedObjectForIUnknown(parentFolder, typeof(IShellFolder));
      }
      catch
      {
        if (pidl != IntPtr.Zero)
        {
          Marshal.FreeCoTaskMem(pidl);
        }

        if (displayNamePtr != IntPtr.Zero)
        {
          Marshal.FreeCoTaskMem(displayNamePtr);
        }

        throw;
      }
    }


    private static IntPtr[] GetPidls(IShellFolder parentFolder, ICollection<string> childNames)
    {
      if (null == childNames || 0 == childNames.Count)
      {
        return null;
      }

      var pidls = new List<IntPtr>();

      try
      {
        foreach (string childName in childNames)
        {
          // Get the file relative to folder
          uint pchEaten = 0;
          SFGAO pdwAttributes = 0;
          IntPtr pidl;

          int result = parentFolder.ParseDisplayName(IntPtr.Zero, IntPtr.Zero, childName, ref pchEaten, out pidl,
                                                     ref pdwAttributes);

          if (S_OK != result)
          {
            Marshal.ThrowExceptionForHR(result);
          }

          pidls.Add(pidl);
        }
      }
      catch
      {
        FreePidls(pidls.ToArray());

        throw;
      }

      return pidls.ToArray();
    }


    /// <summary>
    /// Free the PIDLs
    /// </summary>
    /// <param name="pidls">Array of PIDLs (IntPtr)</param>
    private static void FreePidls(IntPtr[] pidls)
    {
      if (null == pidls)
      {
        return;
      }

      for (int n = 0; n < pidls.Length; n++)
      {
        if (pidls[n] == IntPtr.Zero)
        {
          continue;
        }

        Marshal.FreeCoTaskMem(pidls[n]);
        pidls[n] = IntPtr.Zero;
      }
    }


    public void Show()
    {
      _menuHandle = IntPtr.Zero;
      _contextMenuPtr = IntPtr.Zero;
      _pidls = new IntPtr[] {};
      _contextMenu = null;
      _parentFolder = null;
      _desktopFolder = null;
      _items = new List<ShellContextMenuItem>();

      try
      {
        _desktopFolder = GetDesktopFolder();
        _parentFolder = GetParentFolder(_desktopFolder, ParentFolderPath);

        _pidls = GetPidls(_parentFolder, _fileSystemItemsNames);

        _contextMenu = GetContextMenuInterface(_parentFolder, _pidls, out _contextMenuPtr);

        _menuHandle = CreatePopupMenu();

        int result = _contextMenu.QueryContextMenu(_menuHandle, 0, _firstCommand, _lastCommand,
                                                   CMF.EXPLORE | CMF.NORMAL | CMF.EXTENDEDVERBS);

        if (result != S_OK)
        {
          Marshal.ThrowExceptionForHR(result);
        }

        Marshal.QueryInterface(_contextMenuPtr, ref IID_IContextMenu2, out _contextMenuPtr2);
        Marshal.QueryInterface(_contextMenuPtr, ref IID_IContextMenu3, out _contextMenuPtr3);

        _contextMenu2 = (IContextMenu2)Marshal.GetTypedObjectForIUnknown(_contextMenuPtr2, typeof(IContextMenu2));
        _contextMenu3 = (IContextMenu3)Marshal.GetTypedObjectForIUnknown(_contextMenuPtr3, typeof(IContextMenu3));

        POINT pointScreen;
        User32Methods.GetCursorPos(out pointScreen);

        uint nSelected = User32Methods.TrackPopupMenuEx(_menuHandle, TPM.RETURNCMD, pointScreen.X, pointScreen.Y,
                                                        _windowInteropHelper.Handle, IntPtr.Zero);

        DestroyMenu(_menuHandle);
        _menuHandle = IntPtr.Zero;

        if (nSelected != 0)
        {
          InvokeCommand(_contextMenu, nSelected, ParentFolderPath, pointScreen);
        }
      }
      catch
      {
        // TODO <RNovitsky, 04.06.2009>: Log this

        ReleaseAll();
        _items.Clear();
      }
    }


    /// <summary>
    /// This method receives WindowMessages. It will make the "Open With" and "Send To" work
    /// by calling HandleMenuMsg and HandleMenuMsg2. It will also call the OnContextMenuMouseHover
    /// method of Browser when hovering over a ContextMenu item.
    /// </summary>
    /// <param name="handle">An instance of a <see cref="System.IntPtr"/>.</param>
    /// <param name="message">the Message of the Browser's WndProc</param>
    /// <param name="wParam">An instance of a <see cref="System.IntPtr"/>.</param>
    /// <param name="lParam">An instance of a <see cref="System.IntPtr"/>.</param>
    /// <param name="handled">if set to <c>true</c> the message has been handled.</param>
    private IntPtr WndProc(IntPtr handle, int message, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
      #region IContextMenu2


      if (_contextMenu2 != null
          && (message == (int)WM.INITMENUPOPUP || message == (int)WM.MEASUREITEM || message == (int)WM.DRAWITEM))
      {
        if (_contextMenu2.HandleMenuMsg((uint)message, wParam, lParam) == S_OK)
        {
          handled = true;
          return IntPtr.Zero;
        }
      }


      #endregion


      #region IContextMenu3


      if (_contextMenu3 != null && message == (int)WM.MENUCHAR)
      {
        if (_contextMenu3.HandleMenuMsg2((uint)message, wParam, lParam, IntPtr.Zero) == S_OK)
        {
          handled = true;
          return IntPtr.Zero;
        }
      }


      #endregion


      handled = false;
      return IntPtr.Zero;
    }


    private static List<ShellContextMenuItem> GetItems(IntPtr menuHandle)
    {
      var items = new List<ShellContextMenuItem>();

      int count = GetMenuItemCount(menuHandle);

      for (uint i = 0; i < count; i++)
      {
        var text = new string('e', 256);

        var menuItemInfo = new MENUITEMINFO(text);
        menuItemInfo.fMask = MIIM.SUBMENU | MIIM.FTYPE | MIIM.STRING | MIIM.STATE | MIIM.ID | MIIM.DATA
                             | MIIM.CHECKMARKS | MIIM.BITMAP;

        try
        {
          int result = GetMenuItemInfo(menuHandle, i, true, ref menuItemInfo);

          if (result != S_OK)
          {
            Marshal.ThrowExceptionForHR(result);
          }
        }
        catch
        {
          continue;
        }

        if (string.IsNullOrEmpty(menuItemInfo.dwTypeData) || menuItemInfo.dwTypeData == text)
        {
          continue;
        }

        var subMenuItems = new List<ShellContextMenuItem>();

        if (menuItemInfo.hSubMenu != IntPtr.Zero)
        {
          subMenuItems = GetItems(menuItemInfo.hSubMenu);
        }

        var contextMenuItem = new ShellContextMenuItem(menuItemInfo, subMenuItems);
        items.Add(contextMenuItem);
      }

      return items;
    }


    private void ReleaseAll()
    {
      if (_menuHandle != IntPtr.Zero)
      {
        DestroyMenu(_menuHandle);
      }

      if (_contextMenuPtr != IntPtr.Zero)
      {
        Marshal.Release(_contextMenuPtr);
      }

      if (_contextMenuPtr2 != IntPtr.Zero)
      {
        Marshal.Release(_contextMenuPtr2);
      }

      if (_contextMenuPtr3 != IntPtr.Zero)
      {
        Marshal.Release(_contextMenuPtr3);
      }

      if (null != _contextMenu)
      {
        Marshal.ReleaseComObject(_contextMenu);
      }

      if (null != _contextMenu2)
      {
        Marshal.ReleaseComObject(_contextMenu2);
      }

      if (null != _contextMenu3)
      {
        Marshal.ReleaseComObject(_contextMenu3);
      }

      if (null != _parentFolder)
      {
        Marshal.ReleaseComObject(_parentFolder);
      }

      if (null != _desktopFolder)
      {
        Marshal.ReleaseComObject(_desktopFolder);
      }

      FreePidls(_pidls);
    }


    // Retrieves the IShellFolder interface for the desktop folder, which is the root of the Shell's namespace.
    [DllImport("shell32.dll")]
    private static extern Int32 SHGetDesktopFolder(out IntPtr ppshf);


    // Takes a STRRET structure returned by IShellFolder::GetDisplayNameOf, converts it to a string, and places the result in a buffer. 
    [DllImport("shlwapi.dll", EntryPoint = "StrRetToBuf", ExactSpelling = false, CharSet = CharSet.Auto,
      SetLastError = true)]
    private static extern Int32 StrRetToBuf(IntPtr pstr, IntPtr pidl, StringBuilder pszBuf, int cchBuf);


    // The CreatePopupMenu function creates a drop-down menu, submenu, or shortcut menu. The menu is initially empty. You can insert or append menu items by using the InsertMenuItem function. You can also use the InsertMenu function to insert menu items and the AppendMenu function to append menu items.
    [DllImport("user32", SetLastError = true, CharSet = CharSet.Auto)]
    private static extern IntPtr CreatePopupMenu();


    // The DestroyMenu function destroys the specified menu and frees any memory that the menu occupies.
    [DllImport("user32", SetLastError = true, CharSet = CharSet.Auto)]
    private static extern bool DestroyMenu(IntPtr hMenu);


    [DllImport("user32", SetLastError = true, CharSet = CharSet.Auto)]
    private static extern int GetMenuItemInfo(IntPtr hMenu, uint uItem, bool fByPosition, ref MENUITEMINFO lpmii);


    [DllImport("user32", SetLastError = true, CharSet = CharSet.Auto)]
    private static extern int GetMenuItemCount(IntPtr hMenu);


    public void Dispose()
    {
      Dispose(true);
      GC.SuppressFinalize(this);
    }


    private void Dispose(bool disposing)
    {
      if (disposing)
      {
        _fileSystemItemsNames.Clear();
        _items.Clear();

        WindowHookHelper.RemoveHook(_window, WndProc);
      }

      ReleaseAll();
    }


    #endregion
  }
}